"""
Math Inspector: a visual programming environment for scientific computing
Copyright (C) 2021 Matt Calhoun

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""

import tkinter as tk
import numpy as np
import inspect, sys, traceback, os, re, plot
from code import InteractiveInterpreter
from util import vdict, open_editor, BUTTON_RIGHT, BASEPATH, FONT
from version import VERSION
from doc import Help
from style import Color, TAGS
from types import CodeType
from widget import Text, Menu
from .prompt import Prompt, FONTSIZE
from .codeparser import CodeParser
from .builtin_print import builtin_print

RE_TRACEBACK = r"^()(Traceback \(most recent call last\))"
RE_FILEPATH = r"(File (\"(?!<).*\"))"
RE_INPUT = r"(File \"(<.*>)\")"
RE_LINE = r"((line [0-9]*))"
RE_IN = r"line ([0-9]*), in (.*)"

class Interpreter(Text, InteractiveInterpreter):
	"""
	This class extends `InteractiveInterpreter` from the module `code`,
	and follows the official patterns for creating python interpreters.

	In order to synchronize all the different views with the interpreter, the local namespace
	is stored in a vdict called `self.locals`.  A vdict is key-value pair, just like a regular 
	dictionary, except it also has callbacks for events such as setting and deleting items. 

	When running commands, `self.locals` is passed to the builtin `exec` function as the third parameter.  
	This parameter determines the local namespace exec runs in.  You can display the current
	contents of the local namespace by running the command

	>>> locals()

	When exec processses code, it updates the values in `self.locals`, but since this is a vdict, 
	the callbacks are automatically triggered.

	The math inspector console extends the traditional python interpreter, and has many quality of 
	life improvements such as syntax highlighting and a wide variety of hotkeys. Before and 
	after each command is executed, the command string is parsed to keep the other views 
	synchronized with the variables in the local namespace.  Code parsing is done 
	using the abstract syntax tree module `ast`.  An abstract syntax tree is generated by 
	python under the hood as an intermediate step between processing 
	lines of code and executing those commands.

	An excellent resource for learning about abstract syntax tree's is the 
	website https://greentreesnakes.readthedocs.io/	
	"""
	def __init__(self, app):	
		InteractiveInterpreter.__init__(self, vdict({
			"app": app,
			"plot": plot.plot
		}, setitem=self.setitem, delitem=self.delitem))

		self.frame = tk.Frame(app, 
			padx=16,
			pady=8,
			background=Color.DARK_BLACK)

		Text.__init__(self, self.frame,
			readonly=True, 
			background=Color.DARK_BLACK, 
			font=FONT,
			padx=0,
			pady=0,
			wrap="word",
			cursor="arrow", 
			insertbackground=Color.DARK_BLACK)

		sys.displayhook = self.write
		sys.excepthook = self.showtraceback
		__builtins__["print"] = self.write		
		__builtins__["help"] = Help(app)
		__builtins__["clear"] = Clear(self)
		__builtins__["copyright"] = "Copyright (c) 2018-2021 Matt Calhoun.\nAll Rights Reserved."
		__builtins__["credits"] = "Created by Matt Calhoun.\nSee https://mathinspector.com for more information."
		__builtins__["license"] = License()

		self.app = app
		self.prompt = Prompt(self, self.frame)
		self.parse = CodeParser(app)
		self.buffer = []
		self.prevent_module_import = False

		self.bind("<Key>", self._on_key_log)
		self.bind("<Configure>", self.prompt.on_configure_log)
		self.bind("<ButtonRelease-1>", self.on_click_log)
		self.pack(fill="both", expand=True)
		
		for i in ["error_file"]:
			self.tag_bind(i, "<Motion>", lambda event, key=i: self._motion(event, key))
			self.tag_bind(i, "<Leave>", lambda event, key=i: self._leave(event, key))
			self.tag_bind(i, "<Button-1>", lambda event, key=i: self._click(event, key))    

	def _init(self, event):
		self.config_count = self.config_count + 1 if hasattr(self, "config_count") else 1
		if self.config_count > 2:
			self.do_greet()
			self.prompt.place(width=self.winfo_width())
			self.bind("<Configure>", self.prompt.on_configure_log)
		
	def do_greet(self):
		self.write("Math Inspector " + VERSION + " (Beta)\nType \"help\", \"copyright\", \"credits\", \"license\" for more information")	
		self.prompt()

	def setitem(self, key, value):
		if inspect.ismodule(value):
			if not self.prevent_module_import:
				self.app.modules[key] = value
		else:
			self.app.objects[key] = value

	def delitem(self, key, value):
		if inspect.ismodule(value):
			del self.app.modules[key]
		else:
			del self.app.objects[key]

	def synclocals(self):
		self.locals.store.update(self.app.objects.store)
		self.locals.store.update(self.app.modules.store)

	def eval(self, source):
		self.synclocals()
		return eval(source, self.locals)

	def exec(self, source, filename="<file>"):
		self.prevent_module_import = True
		self.synclocals()
		self.parse.preprocess(source)
		self.runsource(source, filename, "exec")
		self.parse.postprocess(source)
		self.prevent_module_import = False

	def push(self, s, filename="<input>", log=True, symbol="single"):
		self.synclocals()
		source = "".join(self.buffer + [s])
		self.parse.preprocess(source)
		if source[:4] == "plot":
			self.prompt()
		did_compile = self.runsource(source, filename, symbol)
		
		if did_compile:
			self.buffer.append(s + "\n")
			self.prompt.history.append(s)
		else:
			self.parse.postprocess(source)
			self.prompt.history.extend(s)
			self.buffer.clear()
		self.prompt()
	
	def write(self, *args, syntax_highlight=False, tags=(), **kwargs):
		idx = self.index("insert")
		for r in args:
			if re.match(RE_TRACEBACK, str(r)):
				tags = ("red", *tags)
			
			if r is not None:
				if isinstance(r, Exception):
					builtin_print("\a")
					tags = tuple(list(tags) + ["red"])
				self.insert("end", str(r), tags, syntax_highlight=syntax_highlight)
				if len(args) > 1:
					self.insert("end", "\t")
		
		try:
			self.highlight(RE_FILEPATH, "error_file", idx)
			self.highlight(RE_INPUT, "purple", idx)
			self.highlight(RE_LINE, "blue", idx)
			self.highlight(RE_IN, "green", idx)
		except:
			pass

		if self.get("1.0", "end").strip():
			self.insert("end", "\n")
		self.see("end")
		self.prompt.move()

	def showtraceback(self, *args):
		sys.last_type, sys.last_value, last_tb = ei = sys.exc_info()
		sys.last_traceback = last_tb

		try:
			lines = traceback.format_exception(ei[0], ei[1], last_tb.tb_next)
			self.write(''.join(lines).rstrip(), tags="red")
			builtin_print ("\a")
			self.app.menu.setview("console", True)
		finally:
			last_tb = ei = None

	def clear(self):
		self.prompt.is_on_bottom = False
		self.delete("1.0", "end")
	
	def _on_key_log(self, event):
		result = self._on_key(event)
		if result:
			self.prompt.focus()
		return result

	def on_click_log(self, event):
		 if not self.tag_ranges("sel"):
		 	self.prompt.focus()

	def _on_button_right(self, event):
		option = []
		tag_ranges = self.tag_ranges("sel")
		if tag_ranges:
			option.extend([{
				"label": "Copy",
				"command": lambda: self.clipboard_append(self.get(*tag_ranges))
			}])

		self.menu.show(event, option + [{
			"label": "Clear Log",
			"command": clear
		}])


	def _click(self, event, tag):
		if not self.hover_range: return
		content = self.get(*self.hover_range)
		if tag == "error_file":
			open_editor(os.path.abspath(content[1:-1]))

class License:
	def __call__(self):
		help(os.path.join(BASEPATH, "LICENSE"))

	def __repr__(self):
		return """
Math Inspector: a visual programming environment for scientific computing

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

Type license() to see the full license text
		"""

class Clear:
	def __init__(self, console):
		self.console = console
	
	def __call__(self):
		self.console.clear()
		self.console.prompt()
	
	def __repr__(self):
		self.console.clear()
		self.console.prompt()
		return ""				
